// Copyright lowRISC contributors (OpenTitan project).
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#include "usbdev_serial.h"

#include <cstdio>
#include <iostream>
#include <unistd.h>

#include "stream_test.h"
#include "usbdev_utils.h"

USBDevSerial::~USBDevSerial() { Stop(); }

// Iniitialise a stream between the specified input and output ports.
bool USBDevSerial::Open(const char *in_name, const char *out_name) {
  // Take a copy of the port names.
  inPort_ = in_name;
  outPort_ = out_name;
  return OpenPorts();
}

// Open the input and output ports to the board/device for this stream.
bool USBDevSerial::OpenPorts() {
  in_ = port_open(inPort_.c_str(), false);
  if (in_ < 0) {
    return false;
  }
  out_ = port_open(outPort_.c_str(), true);
  if (out_ < 0) {
    close(in_);
    return false;
  }

  if (cfg.verbose) {
    printf("S%u: input '%s' (%d) output '%s' (%d)\n", id_, inPort_.c_str(), in_,
           outPort_.c_str(), out_);
  }

  return true;
}

void USBDevSerial::Stop() {
  // Close any open port handles.
  if (in_ >= 0) {
    close(in_);
    in_ = -1;
  }
  if (out_ >= 0) {
    close(out_);
    out_ = -1;
  }
}

void USBDevSerial::Pause() {
  // This is the same behavior as stopping.
  Stop();
}

bool USBDevSerial::Resume() {
  // I don't think we can expect this to suspend/resume at the byte level
  // anyway; we seem to resume at a different point in the LFSR, presumably
  // because some internal buffer within the kernel/driver level dumps some
  // bytes when we close the handle.
  return OpenPorts();
}

// Return a summary report of the stream settings of status.
std::string USBDevSerial::Report(bool status, bool verbose) const { return ""; }

// Sending of OUT traffic to device.
bool USBDevSerial::ServiceOUT() {
  uint8_t *data;
  size_t to_send = DataAvailable(&data);
  if (to_send > 0U) {
    ssize_t nsent;
    if (send_) {
      if (cfg.verbose) {
        std::cout << PrefixID() << "Trying to send " << to_send << "byte(s)"
                  << std::endl;
      }
      // Propagate the modified bytes to the output port.
      nsent = send_bytes(out_, &buf_.data[buf_.rd_idx], to_send);
      if (nsent < 0) {
        return false;
      }
    } else {
      nsent = to_send;
    }

    if (cfg.verbose) {
      std::cout << PrefixID() << (send_ ? "Sent" : "Dropped") << nsent
                << " byte(s)" << std::endl;
    }

    ConsumeData(nsent);
  }

  return true;
}

// Retrieving of IN traffic from device.
bool USBDevSerial::ServiceIN() {
  // Decide how many bytes to try to read into our buffer.
  uint8_t *dp;
  uint32_t space_bytes = SpaceAvailable(&dp);

  uint32_t to_fetch = space_bytes;
  if (to_fetch > transfer_bytes_ - bytes_recvd_) {
    to_fetch = transfer_bytes_ - bytes_recvd_;
  }

  ssize_t nrecvd;
  if (!SigReceived() || retrieve_) {
    // Read as many bytes as we can from the input port.
    nrecvd = recv_bytes(in_, dp, to_fetch);
    if (nrecvd < 0) {
      return false;
    }

    // Update the circular buffer with the amount of data that we've written.
    CommitData(nrecvd);

    if (nrecvd > 0 && !SigReceived()) {
      uint32_t dropped = SigDetect(&sig_, dp, (uint32_t)nrecvd);

      // Consume stream signature, rather than propagating it to the output
      // side.
      if (SigReceived()) {
        SigProcess(sig_);
        dropped += sizeof(usbdev_stream_sig_t);
      }

      // Skip past any dropped bytes, including the signature, so that if there
      // are additional bytes we may process them.
      nrecvd = ((uint32_t)nrecvd > dropped) ? ((uint32_t)nrecvd - dropped) : 0;
      dp += dropped;

      if (dropped) {
        DiscardData(dropped);
      }
    }
  } else {
    // Generate a stream of bytes _as if_ we'd received them correctly from
    // the device.
    GenerateData(dp, to_fetch);
    nrecvd = to_fetch;

    // Update the circular buffer with the amount of data that we've written.
    CommitData(nrecvd);
  }

  bool ok = true;
  if (nrecvd > 0) {
    // Check the received LFSR-generated byte(s) and combine them with the
    // output of our host-side LFSR.
    ok = ProcessData(dp, nrecvd);
  }

  return ok;
}

// Service this stream.
bool USBDevSerial::Service() {
  // The base class may perform some diagnostic reporting common to all streams.
  bool ok = USBDevStream::Service();
  if (ok) {
    // Handle OUT traffic first to try to create more buffer space.
    ok = ServiceOUT();
    if (ok) {
      ok = ServiceIN();
    }
  }
  return ok;
}
